package main

import (
	"encoding/json"
	"fmt"
	"io/fs"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	log "github.com/sirupsen/logrus"
	"github.com/tujiaw/goutil"
)

type Handler struct {
	app *gin.Engine
}

func NewHandler(engine *gin.Engine) Handler {
	return Handler{
		app: engine,
	}
}

func (handler Handler) RedirectContext(location string, c *gin.Context) {
	c.Request.URL.Path = location
	handler.app.HandleContext(c)
}

func (handler Handler) LoggerMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		startTime := time.Now()
		c.Next()
		endTime := time.Now()
		cost := endTime.Sub(startTime)
		method := c.Request.Method
		requrl := c.Request.RequestURI
		log.Info("url:", requrl, ", method:", method, ", cost:", cost.Milliseconds(), "ms")
	}
}

func (handler Handler) ErrorRender(title string, message string, c *gin.Context) {
	backUrl, err := GetRefererPath(c)
	if err != nil {
		title = "Referer Path Error"
		message = err.Error()
		backUrl = HOME_URL
	}

	c.HTML(http.StatusInternalServerError, "error.html", gin.H{
		"title":   title,
		"message": message,
		"back":    backUrl,
	})
}

func (handler Handler) Profile(c *gin.Context) {
	var dirCount = 0
	var fileCount = 0
	var fileTotalSize = int64(0)
	var walkDirFunc = func(path string, d fs.DirEntry, err error) error {
		if d.IsDir() {
			dirCount += 1
		} else {
			fileCount += 1
			fileTotalSize += goutil.GetFileSize(path)
		}
		return nil
	}

	if err := filepath.WalkDir(HOME_DIR, walkDirFunc); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()})
		return
	}

	var lastmodify string
	fi, err := os.Stat(HOME_DIR)
	if err == nil {
		lastmodify = goutil.FromNowUnix(fi.ModTime().Unix())
	}

	c.HTML(http.StatusOK, "profile.html", gin.H{
		"filecount":     fileCount,
		"filetotalsize": goutil.ByteSize(fileTotalSize).Format(),
		"dircount":      dirCount,
		"lastmodify":    lastmodify,
	})
}

func (handler Handler) Home(c *gin.Context) {
	urlInfo, err := url.Parse(c.Param("path"))
	preview := c.Query("preview")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"err": err.Error()})
		return
	}
	urlPath := urlInfo.Path
	localPath := path.Join(HOME_DIR, urlPath)
	log.Info("url path:", urlPath)
	if goutil.IsDir(localPath) {
		fullUrl := path.Join(HOME_URL, urlPath)
		RowItemList, hasMusic := ReadDirFromUrlPath(urlPath, c.Request.URL.RawQuery)
		SortFiles(RowItemList, c.Query("s"), c.Query("o"))
		navList := ParseNavList(fullUrl, c.Request.URL.RawQuery)
		data := gin.H{
			"title":     urlPath,
			"dir":       urlPath,
			"list":      RowItemList,
			"list_len":  len(RowItemList),
			"nav":       navList,
			"has_music": hasMusic,
		}
		c.HTML(http.StatusOK, "index.html", data)
		return
	}

	if !goutil.FileExists(localPath) {
		handler.RedirectContext("/404", c)
		return
	}

	if preview == "file" {
		c.File(localPath)
	} else if preview == "text" {
		const MAX_PREVIEW_LENGTH = 2048
		f, err := os.OpenFile(localPath, os.O_RDONLY, os.ModePerm)
		if err != nil {
			c.Writer.Write([]byte(err.Error()))
			return
		}
		defer f.Close()

		buffer := make([]byte, MAX_PREVIEW_LENGTH)
		n, err := f.Read(buffer)
		if err != nil {
			c.Writer.Write([]byte(err.Error()))
			return
		}
		if n < MAX_PREVIEW_LENGTH {
			c.Writer.Write(buffer[:n])
		} else {
			c.Writer.Write(append(buffer[:n], []byte("......")...))
		}
	} else {
		c.FileAttachment(localPath, path.Base(localPath))
	}
}

func (handler Handler) NoRoute(c *gin.Context) {
	c.HTML(http.StatusOK, "404.html", gin.H{})
}

func (handler Handler) Delete(c *gin.Context) {
	// force允许删除非空目录
	force := false
	if c.Query("force") == "true" {
		force = true
	}

	b, err := ioutil.ReadAll(c.Request.Body)
	if err != nil {
		handler.ErrorRender("Error", err.Error(), c)
		return
	}

	var files []string
	err = json.Unmarshal(b, &files)
	if err != nil {
		handler.ErrorRender("Error", err.Error(), c)
		return
	}

	localPathList := make([]string, 0, len(files))
	enableCount := 0
	for _, f := range files {
		localPath := GetLocalPath(f)
		if escapePath, err := url.QueryUnescape(localPath); err == nil {
			localPath = escapePath
		}
		// 文件或者空目录才允许直接删除
		if !goutil.IsDir(localPath) || goutil.IsPathEmpty(localPath) {
			enableCount += 1
		}
		localPathList = append(localPathList, localPath)
	}

	if !force && enableCount != len(files) {
		c.JSON(http.StatusOK, gin.H{"err": "Directory is not empty, continue?"})
		return
	}

	for _, localPath := range localPathList {
		// 移到垃圾箱中
		trashPath := path.Join(TRASH_DIR, time.Now().Format("20060102150405")+"_"+filepath.Base(localPath))
		if err := os.Rename(localPath, trashPath); err != nil {
			log.Error("rename file, from:", localPath, ", to:", trashPath, ", err:", err)
		}
		// 直接删除
		// if err = os.RemoveAll(localPath); err != nil {
		// 	log.Error("remove file, name:", f, ", err:", err)
		// }
	}
	c.JSON(http.StatusOK, gin.H{
		"err": 0,
	})
}

func (handler Handler) New(c *gin.Context) {
	curPath, err := GetRefererPath(c)
	if err != nil {
		return
	}

	name := c.PostForm("name")
	name = strings.TrimSpace(name)
	if len(name) == 0 {
		handler.ErrorRender("Warning", "The name cannot be empty!", c)
		return
	}

	newPath := path.Join(GetLocalPath(curPath), name)
	if err := os.MkdirAll(newPath, os.ModePerm); err != nil {
		handler.ErrorRender("ERROR", err.Error(), c)
		return
	}

	c.Redirect(http.StatusFound, curPath)
}

func (handler Handler) Upload(c *gin.Context) {
	curPath, err := GetRefererPath(c)
	if err != nil {
		return
	}

	form, err := c.MultipartForm()
	if err != nil {
		handler.ErrorRender("Warning", err.Error(), c)
		return
	}

	files := form.File["files"]
	dst := GetLocalPath(curPath)
	for _, file := range files {
		name := file.Filename
		if escapeName, err := url.QueryUnescape(file.Filename); err == nil {
			name = escapeName
		}
		dstFile := path.Join(dst, name)
		dstFile = GetUniquePath(dstFile)
		if err := c.SaveUploadedFile(file, dstFile); err != nil {
			log.Error("save file, name:", file.Filename, ", err:", err)
		}
	}
	c.Redirect(http.StatusFound, curPath)
}

func (handler Handler) Move(c *gin.Context) {
	curpath, err := GetRefererPath(c)
	if err != nil {
		return
	}

	frompath := strings.TrimSpace(c.PostForm("frompath"))
	dstpath := c.PostForm("name")
	if len(frompath) == 0 || len(dstpath) == 0 {
		handler.ErrorRender("Warning", "File path cannot be empty!", c)
		return
	}

	dstpath = GetLocalPath(dstpath)
	dstdir := filepath.Dir(dstpath)
	if !goutil.FileExists(dstdir) {
		if err := os.MkdirAll(dstdir, os.ModePerm); err != nil {
			handler.ErrorRender("ERROR", err.Error(), c)
			return
		}
	}

	frompath = GetLocalPath(frompath)
	log.Info("remove from:", frompath, ", to:", dstpath)
	if err := os.Rename(frompath, dstpath); err != nil {
		handler.ErrorRender("ERROR", err.Error(), c)
		return
	}

	c.Redirect(http.StatusFound, curpath)
}

func (handler Handler) Archive(c *gin.Context) {
	name := c.PostForm("name")
	if len(name) == 0 {
		handler.ErrorRender("Warning", "The name cannot be empty!", c)
		return
	}

	var pathlist []string
	if err := json.Unmarshal([]byte(c.PostForm("pathlist")), &pathlist); err != nil {
		handler.ErrorRender("ERROR", err.Error(), c)
		return
	}
	log.Info("archive list:", pathlist)

	var zipdir string
	var paramsList []string
	for _, pathstr := range pathlist {
		if escapePath, err := url.QueryUnescape(pathstr); err == nil {
			localpath := GetLocalPath(escapePath)
			if goutil.FileExists(localpath) {
				name := filepath.Base(localpath)
				if len(zipdir) == 0 {
					zipdir = filepath.Dir(localpath)
				}
				paramsList = append(paramsList, "\""+name+"\"")
			}
		}
	}

	uniqueName, err := goutil.Uuidv4()
	if err != nil {
		handler.ErrorRender("ERROR UUID", err.Error(), c)
		return
	}

	zippath := path.Join(ARCHIVE_DIR, uniqueName+"_"+name)
	cmdstr := fmt.Sprintf("cd %s && zip -r %s %s", zipdir, zippath, strings.Join(paramsList, " "))
	log.Info("shell command:", cmdstr)
	cmd := exec.Command("/bin/bash", "-c", cmdstr)
	if cmd == nil {
		handler.ErrorRender("ERROR EXEC SHELL", "Exec command shell failed!", c)
		return
	}

	err = cmd.Run()
	if err != nil {
		handler.ErrorRender("ERROR", err.Error(), c)
		return
	}

	c.FileAttachment(zippath, name)
}
